iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 21
0
自我挑戰組

不用前端框架 手把手打造基礎SPA網站系列 第 21

[DAY21]進階應用 - 監聽事件的處理(下篇)

  • 分享至 

  • xImage
  •  

大家好,延續昨天的主題,我們希望可以對父層元件body使用新增/移除eventListeners來綁定監聽事件,並且需要一個方法,可以在切換頁面後移除曾經註冊監聽的事件,那就廢話不多說,馬上開始今天的主題吧。

建立監聽事件處理器

首先在src目錄下建立一個utils資料夾,專門存放共用程序模組。接著建立eventListener.js,作為處理SPA裡監聽事件的共用模組:

接著我們在eventListener.js開頭,宣告常數eventHandlers,並賦予物件型別輸出為模組(之後Router會用到)。這是用來註冊每個Component內的監聽事件處理器:

src/utils/eventListerer.js

export const eventHandlers = {}

這裡說明一下註冊的方式,我們希望eventHandlers可以紀錄當前元件body的所有監聽事件。紀錄的方式為元件在mount之後,從Component內找出要新增的監聽事件類型與事件處理回呼函式(event handler),註冊到這個eventHandlers物件內,同時對body新增監聽事件(addEventListener)。之後切換到別的頁面,會把前次在eventHandlers物件所註冊的監聽事件做取消,同時移除監聽事件(removeEventListener),之後重複循環下去。如此在不同頁面綁定的監聽事件,會隨著切換頁面自動移除,不用擔心重複因新增監聽事件而影響效能。

處理新增監聽事件

承上,我們需要建立一個新增監聽事件函式並且匯出,作用是可以將監聽的事件註冊到eventHandlers物件內,同時能夠對body新增監聽事件:

src/utils/eventListerer.js

//對body增加監聽事件
export const addListener = (type, handler, capture = false) => {
  //判斷若沒該事件類型,則新增事件屬性並賦予陣列初始值
  if (!(type in eventHandlers)) {
    eventHandlers[type] = []
  }
  //註冊handlers
  eventHandlers[type].push({
    handler: handler,
    capture: capture,
  })
  //對body新增監聽
  document.querySelector('body').addEventListener(type, handler, capture)
}

這裡可以看到addListener函式有三個參數可以設置,跟addEventListener裡的參數是一樣的(詳細用法可參閱MDN):

  • type:表示監聽事件類型的字符串。
  • handler:監聽執行的函式,當所監聽的事件類型觸發時,會執行這段函式。
  • capture(預設值為false):將handler設置為capture(捕捉)與bubble(冒泡)的階段,true表示新增為capture階段,false表示新增為bubble階段。

在這裡可以看到eventHandlers註冊的資料格式,屬性為事件類型,值的部份為陣列,裡面每個元素為物件,包含handler與capture:

type:[{
  handler: handler,
  capture: capture,
}]

處理移除監聽事件

接著我們需要再建立一個移除監聽事件函式並匯出,作用是可以把上一次在eventHandlers內註冊的監聽事件做取消,同時移除body的監聽事件:

src/utils/eventListerer.js

//移除body所有監聽事件
export const removeAllListeners = () => {
  if (Object.keys(eventHandlers).length) {
    //逐個對eventHandlers內的事件類型處理handler
    Object.keys(eventHandlers).forEach((type) => {
      //逐個對事件類型註冊的handler做處理
      eventHandlers[type].forEach(({ handler, capture }) =>
        //移除監聽
        document
          .querySelector('body')
          .removeEventListener(type, handler, capture)
      )
      //將eventHandlers裡的事件回復成初始值
      eventHandlers[type] = []
    })
  }
}

為了對物件內所有事件類型做處理,希望可以輸出陣列搭配forEach做執行,所以用Object.keys()方法,參閱MDN的說明如下:

Object.keys() 方法會回傳一個由指定物件所有可列舉之屬性組成的陣列,該陣列中的的排列順序與使用 for...in 進行迭代的順序相同(兩者的差異在於 for-in 迴圈還會迭代出物件自其原型鏈所繼承來的可列舉屬性)。

我們可以看到forEach會逐個移除曾註冊過的監聽事件,handler是前次新增的,所以不會像昨天一樣找不到同一個。

元件內註冊監聽事件

監聽模組建立好了,接下來我們看看如何在Component內註冊監聽事件。首先在Post內新增一個listener的屬性:

src/pages/Post.js

export const Post = {
  listener: {
    click: (e) => {
      if (e.target.id === 'button') {
        console.log('post button clicked')
      }
    },
  },
  //...

listener物件專門用來新增元件內的監聽事件。新增監聽事件可以在內部新增事件類型為屬性,值的部份賦予處理監聽事件的handler。如同昨天button的例子,因為是對body綁定監聽事件,這邊使用event.target來判斷冒泡階段的目標。

router加入/移除監聽處理

以上設置好監聽事件註冊模組,也在元件內增加了監聽事件,最後一步就是在Router裡來處理這些行為:

src/routes/Router.js

//引入監聽事件處理模組
import {
  eventHandlers,
  addListener,
  removeAllListeners,
} from '../utils/eventListerer'
 
export const Router = () => {
  //...
  // 4.元件render後呼叫
  'mount' in component ? component.mount() : null
  // 5.處理監聽事件
  // 取消全部監聽事件
  removeAllListeners()
 
  // 註冊元件內監聽事件
  'listener' in component
    ? Object.keys(component.listener).forEach((type) =>
        addListener(type, component.listener[type])
      )
    : null
 
  // 查看handlers
  console.log('eventHandlers:', eventHandlers)
}

我們開頭把監聽事件處理模組所有方法進行匯入,在mount後新增第五步處理監聽事件。首先用removeAllListeners移除之前所有註冊的監聽事件,然後使用addListener對元件內綁定的監聽事件,依照事件類型依序做註冊與新增。另外這裡運到三元運算子,判斷listener屬性是否存在該元件內,防止沒有屬性時瀏覽器報錯。

最後為了確認eventHandlers註冊的內容,使用了console.log輸出來查看,馬上來看看成果會是怎麼樣:

在切換頁面時,可以清楚看到註冊監聽事件模組的eventHandlers資料格式(也就是之前在addListener註冊的資料格式),這裡不用事件目標當作屬性,而是使用監聽事件類型,值的部份為陣列格式,物件元素內包含handler與capture:

然後我們再試試對Post頁面裡的button進行點擊:

一切正常運作,可以看到切換頁面時,綁定body的新增監聽事件不會像之前一樣越來越多,因為監聽模組與Router都處理好了。

後記

自己在使用React時,可以運用Inline events對目標綁定監聽事件,但自己用原生javascript實做SPA的過程中,發現並沒有這麼簡單,參考網路上的方法後,考量有效管理代碼與不影響效能,做出這套管理監聽事件的方法。但因為是對父層元件body監聽,運用事件捕捉與冒泡來判斷目標,所以也需要對目標元素新增屬性標籤(attribute)來判斷目標本身,不像是React直接綁定在元素比較直覺。

參考資料:

  1. Javascript/DOM: How to remove all events of a DOM object?
  2. [DOM] Event Propagation I : 事件捕捉和冒泡-Event Capture & Bubble

上一篇
[DAY20]進階應用 - 監聽事件的處理(上篇)
下一篇
[DAY22]進階應用 - 元件內部共用函式調用
系列文
不用前端框架 手把手打造基礎SPA網站30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言